package jmpo.cls;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import javax.annotation.processing.SupportedAnnotationTypes;
import javax.management.RuntimeErrorException;

import jmpo.cls.AnnotationExample.Assert;

public final class AnnotationExample {
    /**
     * Ниже представлена аннотация в виде вложенного статического класса.
     * Собственные аннотации вводятся с помощью ключевого слова @interface.
     * Обратите внимание на знак '@'! 
     */
    @Target(ElementType.METHOD) // мы будем добавлять эту аннотацию к методам
    @Retention(RetentionPolicy.RUNTIME) // означает, что данная аннотация будет обрабатываться во время исполнения
    public static @interface Assert {
    }

    /**
     * Аннотация @Assert не содержит никаких полений и методов.
     * Она лишь маркирует некторый метод.
     * Мы будем обрабатывать эту аннотацию с помощью рефлекции.
     * А именно:
     *     мы возьмём рефлекцию нашего класса,
     *     найдём все статические методы,
     *     узнаем кто из них аннотирован как @Assert,
     *     проверим, что эти методы возвращают boolean,
     *     по очереди вызовем эти методы и для каждого выведем сообщение с результатом вызова.
     */
    private static void processClass(Class<?> clazz) {
        Method[] methods = clazz.getDeclaredMethods();
        for (var m : methods) {
            if ( 
                Modifier.isStatic(m.getModifiers()) 
                && m.isAnnotationPresent(Assert.class) ) {
                    processMethod(m);
                }
        }
    }

    private static void processMethod(Method m) {
        if (!Boolean.TYPE.equals(m.getReturnType()))
            throw new RuntimeException(String.format("method '%s' is annotated @Assert, but do not return boolean", m.getName()));
        if (m.getParameterTypes().length > 0)
            throw new RuntimeException(String.format("method '%s' is annotated @Assert, but expect some input parameters", m.getName()));
        try {
            Boolean b = (Boolean) m.invoke(null);
            if (b) {
                System.out.println(String.format("'%s' - Ok", m.getName()));
            } else {
                System.out.println(String.format("'%s' - Failed", m.getName()));
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException(String.format("method '%s' is annotated @Assert, but is not accessible", m.getName()), e);
        } catch (Exception e) {
            throw new RuntimeException(String.format("Exception in @Assert processing with method '%s' invocation", m.getName()), e);
        }
    }

    private static class AssertTest {
        @Assert
        public static boolean testTrue() {
            return true;
        }

        @Assert
        public static boolean testFalse () {
            return false;
        }

        public static boolean testNotInvoked() {
            throw new RuntimeException("testNotInvoked should never been called");
        }
    }

    private static class AssertWrongReturnTypeTest {
        @Assert
        public static void testReturnType() {
            throw new RuntimeException("AssertWrongReturnTypeTest.test should never been called");
        }
    }

    private static class AssertWrongInputParametersTest {
        @Assert
        public static boolean testInputParameters(Boolean b) {
            throw new RuntimeException("AssertWrongInputParametersTest.test should never been called");
        }
    }

    public static void main(String[] args) {
        processClass(AssertTest.class);

        try {
            processClass(AssertWrongReturnTypeTest.class);
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }

        try {
            processClass(AssertWrongInputParametersTest.class);
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
    }
}
